Capítulo 3.2. DECLARACIONES

Capítulo 3.2. Declaraciones

En python todo es un objeto.

Una variable es también un objeto, es decir, una instancia en memoria de una clase.

>>> a = 42
>>> a
    42

>>> type(a)
    <class 'int'>
>>> a = 42
>>> a
    42

>>> type(a)
    <class 'int'>

a es un puntero que apunta a una instancia de la clase int cuyo valor en memoria es 42

>>> b = a  # b es un apuntador al mismo valor que a
>>> b is a
    True
>>> id(a)  # dirección de memoria
94309428040480  
>>> id(b)  # dirección de memoria
94309428040480  

    
>>> del a  # elimina el apuntador a. El contenido sigue en b
>>> b
    42
>>> b is a
    False
>>> b = a  # b es un apuntador al mismo valor que a
>>> b is a
    True
>>> id(a)  # dirección de memoria
94309428040480  
>>> id(b)  # dirección de memoria
94309428040480  

    
>>> del a  # elimina el apuntador a. El contenido sigue en b
>>> b
    42
>>> b is a
    False

Al asignar un nuevo valor a una variable, se está reasignando un nuevo contenido. La memoria ocupada por el anterior valor será liberada cuando el recolector de basura determine.

>>> a = 42  # asigna memoria para el valor 42
>>> id(a)
94309428041760

>>> a = 23  # asigna nueva memoria para el valor 23
>>> id(a)
94309428041472
>>> a = 42  # asigna memoria para el valor 42
>>> id(a)
94309428041760

>>> a = 23  # asigna nueva memoria para el valor 23
>>> id(a)
94309428041472

Por temas de rendimiento, una variable es mutable si se puede cambiar su valor en memoria, o no mutable si se crea otra asignación de memoria y se cambia el puntero.

Variable no mutable, reasignación:

>>> a = (1,)
>>> id(a)
139803025005584
>>> a+=(2,)
>>> id(a)
139803001628064
>>> a+=(3,)
>>> id(a)
139803024299744
>>> a
(1, 2, 3)
>>> a = (1,)
>>> id(a)
139803025005584
>>> a+=(2,)
>>> id(a)
139803001628064
>>> a+=(3,)
>>> id(a)
139803024299744
>>> a
(1, 2, 3)

Variable mutable, cambio en memoria:

>>> a = [1]
>>> id(a)
139803001791344
>>> a+=[2]
>>> id(a)
139803001791344
>>> a+=[3]
>>> id(a)
139803001791344
>>> a
[1, 2, 3]
>>> a = [1]
>>> id(a)
139803001791344
>>> a+=[2]
>>> id(a)
139803001791344
>>> a+=[3]
>>> id(a)
139803001791344
>>> a
[1, 2, 3]

Mejor rendimiento en una n-lista que en una n-tupla. Aunque no son equivalentes, cada estructura tiene usos distintos.

Los métodos sobre variables mutables devuelven nada None, pues modifican el propio objeto.

>>> a = [1,3,2]
>>> print(a.sort()) # ordena la lista y devuelve nada
None  
>>> a
[1, 2, 3]
>>> a = [1,3,2]
>>> print(a.sort()) # ordena la lista y devuelve nada
None  
>>> a
[1, 2, 3]

Los métodos sobre variables no-mutables devuelven un nuevo objeto, y dejan sin modificar el original

>>> a = "Ejemplo"
>>> b = a.lower()  # devuelve un puntero a un nuevo objeto
>>> a
'Ejemplo'
>>> b
'ejemplo'
>>> a = "Ejemplo"
>>> b = a.lower()  # devuelve un puntero a un nuevo objeto
>>> a
'Ejemplo'
>>> b
'ejemplo'

Error común, asignar el resultado de una operación mutable (none) y perder el objeto

>>> a = [1,3,2]
>>> a = a.sort() # ordena la lista y devuelve None
>>> a
None  # hemos perdido la lista :(
>>> a = [1,3,2]
>>> a = a.sort() # ordena la lista y devuelve None
>>> a
None  # hemos perdido la lista :(

Bloques

Las variables no se declaran, por lo tanto si se usan dentro de un bloque, pudiera ser que fuera no existieran, por ejemplo:

>>> if False:
...     c=1
... 
>>> c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined
>>> if False:
...     c=1
... 
>>> c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined

La variable c no existe pues se define en un bloque que nunca entra.

Para saber si una variable está definida:

>>> if 'a' in globals().keys():
        del a
>>> if 'a' in globals().keys():
        del a

Variables definidas dentro de funciones

>>> a = 42      # variable global
>>> def f():
...     a = 34  # variable nueva, local a f()
        print(a, globals.get('a'))  # 34 42
... 
>>> f()
>>> a  # variable global
42
>>> a = 42      # variable global
>>> def f():
...     a = 34  # variable nueva, local a f()
        print(a, globals.get('a'))  # 34 42
... 
>>> f()
>>> a  # variable global
42

No se permite cambiar el valor de una variable global desde dentro de una función.

Funciones

se declaran con def nombre([param]):
Es recomendable documentar en la 1ª línea usando cadenas 3 comillas """Función que hace foo y devuelve faa""""

>>> def f(a,b,c):
... 	"""Devuelve la suma de los tres parámetros"""	
...     return a+b+c
... 
>>> f(2,3,4)
9

>>> def f(a,b,c=0):
... 	"""Devuelve suma de 3 parámetros, el c es opcional"""
...     return a+b+c
... 
>>> f(2,4)
6


>>> def f(a=0,b=0,c=0):
... 	"""Devuelve suma de 3 parámetros opcionales"""
...     return a+b+c
... 
>>> f()
0
>>> f(2,4)
6
>>> f(2,3,4)
9
>>> f(3)
3
>>> def f(a,b,c):
... 	"""Devuelve la suma de los tres parámetros"""	
...     return a+b+c
... 
>>> f(2,3,4)
9

>>> def f(a,b,c=0):
... 	"""Devuelve suma de 3 parámetros, el c es opcional"""
...     return a+b+c
... 
>>> f(2,4)
6


>>> def f(a=0,b=0,c=0):
... 	"""Devuelve suma de 3 parámetros opcionales"""
...     return a+b+c
... 
>>> f()
0
>>> f(2,4)
6
>>> f(2,3,4)
9
>>> f(3)
3

Una función no debería tener efectos colaterales, cada vez que se llame sin argumentos, debe devolver el mismo resultado. Pero... si el argumento por defecto es mutable (por ejemplo una lista), en dos llamadas se estará usando la misma lista, modificada de la anterior llamada.

>>> def f(arg=[1,2]):
        arg += (arg[-1] + 1,)
        print(arg)

>>> f()
[1,2,3]
>>> f()
[1,2,3,4]
>>> def f(arg=[1,2]):
        arg += (arg[-1] + 1,)
        print(arg)

>>> f()
[1,2,3]
>>> f()
[1,2,3,4]

Por eso no es recomendable usar argumentos por defecto modificables. En caso necesario usar uno no-modificable (llamado centinela) y crear el valor por defecto en el cuerpo de la función:

>>> def f(arg=None):
        if arg is None: 
            arg = [1,2]
        arg += (arg[-1] + 1,)
        print(arg)

>>> f()
[1,2,3]
>>> f()
[1,2,3]
>>> def f(arg=None):
        if arg is None: 
            arg = [1,2]
        arg += (arg[-1] + 1,)
        print(arg)

>>> f()
[1,2,3]
>>> f()
[1,2,3]

Parámetros nombrados

Los parámetros opcionales van al final, si queremos dar valor al segundo parámetro sin dar valor al primero tendremos que nombrarlo.

>>> def f(a=0, b=0, c=0):
    print(locals())

>>> f(0,4)   # da valor a 'b' pero también ha de darlo a 'a'
{'a': 0, 'c': 0, 'b': 4}
>>> f(b=4)  # da valor sólo a 'b'
{'a': 0, 'c': 0, 'b': 4}
>>> def f(a=0, b=0, c=0):
    print(locals())

>>> f(0,4)   # da valor a 'b' pero también ha de darlo a 'a'
{'a': 0, 'c': 0, 'b': 4}
>>> f(b=4)  # da valor sólo a 'b'
{'a': 0, 'c': 0, 'b': 4}

Se pueden usar nombrados y no nombrados

# estas llamadas son equivalentes
>>> f(1,2,3)
>>> f(a=1, b=2, c=3)
>>> f(b=2, c=3, a=1)
>>> f(1, 2, c=3)
# estas llamadas son equivalentes
>>> f(1,2,3)
>>> f(a=1, b=2, c=3)
>>> f(b=2, c=3, a=1)
>>> f(1, 2, c=3)

Los nombrados deben pasarse después de los nombrados, y no se debe declarar más de una vez cada argumento:

>>> f(a=1, 2, 3)
  File "<input>", line 1
SyntaxError: non-keyword arg after keyword arg

>>> f(1, 2, a=3)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    f(1, 2, a=3)
TypeError: f() got multiple values for keyword argument 'a'
>>> 
>>> f(a=1, 2, 3)
  File "<input>", line 1
SyntaxError: non-keyword arg after keyword arg

>>> f(1, 2, a=3)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    f(1, 2, a=3)
TypeError: f() got multiple values for keyword argument 'a'
>>> 

Parámetros extensibles

Para funciones que acepten un número variable de argumentos. declararemos los no nombrados en una n-tupla, y los nombrados en un diccionario:

>>> def f(*args):  # n-tupla de parámetros no-nombrados
...     return locals()
... 
>>> f(1,2)  
{'args': (1, 2)}  # args es una tupla de 2 enteros (int,int)
>>> f(1,2,4.5,'a')
{'args': (1,2,4.5,'a')}  # args es una tupla (int,int,float,char)


>>> def g(**kwargs):  # diccionario de parámetros nombrados
...     return locals()
... 
>>> g(a=2,b=3)
{'kwargs': {'a': 2, 'b': 3}} # kwargs es un diccionario de 2 elementos
>>> g(a=2,b=3,c='a')
{'kwargs': {'a': 2, 'c': 'a', 'b': 3}}  # es un diccionario de 3 elementos de distintos tipos
>>> 
>>> def f(*args):  # n-tupla de parámetros no-nombrados
...     return locals()
... 
>>> f(1,2)  
{'args': (1, 2)}  # args es una tupla de 2 enteros (int,int)
>>> f(1,2,4.5,'a')
{'args': (1,2,4.5,'a')}  # args es una tupla (int,int,float,char)


>>> def g(**kwargs):  # diccionario de parámetros nombrados
...     return locals()
... 
>>> g(a=2,b=3)
{'kwargs': {'a': 2, 'b': 3}} # kwargs es un diccionario de 2 elementos
>>> g(a=2,b=3,c='a')
{'kwargs': {'a': 2, 'c': 'a', 'b': 3}}  # es un diccionario de 3 elementos de distintos tipos
>>> 

Al ir nombrados se puede pasar del diccionario a variables unitarias usando:

>>> def g(**kwargs):
...     locals().update(**kwargs)
...     del kwargs
...     return locals()
... 
>>> g(a=2,b=3.3,c='hola')
{'a': 2, 'c': 'hola', 'b': 3.3}
>>> def g(**kwargs):
...     locals().update(**kwargs)
...     del kwargs
...     return locals()
... 
>>> g(a=2,b=3.3,c='hola')
{'a': 2, 'c': 'hola', 'b': 3.3}

Se puede combinar todos los tipos de parámetros en una misma función. En este caso hay que prestar atención al orden de los parámetros

>>> def f(a, b=0, *unnamed, **named):
...     """un obligatorio, un opcional, una tupla variable sin nombre, un diccionario variable con nombre"""
...     return a + b + sum(unnamed) + sum(named.values())
... 
>>> f(1, 2, 3, 4, y=5, z=6)
21
>>> def f(a, b=0, *unnamed, **named):
...     """un obligatorio, un opcional, una tupla variable sin nombre, un diccionario variable con nombre"""
...     return a + b + sum(unnamed) + sum(named.values())
... 
>>> f(1, 2, 3, 4, y=5, z=6)
21

Paso de parámetros con asterisco (unpacking)

Se denomina unpacking a la técnica de pasar una colección de argumentos no nombrados mediante un asterisco, y una colección de diccionario nombrado mediante dos asteriscos. Es útil en más sitios que en el paso de parámetros, por ejemplo, en el caso de querer fusionar varios diccionarios.

Nota, si en varios diccionarios se usa dos veces el mismo nombre de variable, únicamente tendrá validez la última (machaca los valores anteriores).

Firma universal

Una función que acepte absolutamente cualquier parámetro tendría esta firma: def f(*args, **kwargs):

Sin embargo, si hay parámetros obligatorios, habrá que ponerlos antes: def f(a, b, *args, **kwargs):

La ventaja es que si modificamos la firma de esta manera, mantendremos la compatibilidad con código anterior sin tener que revisar todo el código fuente.

El problema es que se pierde el control de las llamadas, es decir, el compilador no nos avisará si estamos llamando a un método con más parámetros que los necesarios. Es recomendable no usar los argumentos con asteriscos

Obligar a un parámetro a ser nombrado (keyword-only)

pdte

Anotaciones

En C++ se indica el tipo de los parámetros, en python no.

Puede ser una ventaja ya que dará igual que el tipo sea un apuntador a fichero, un id flujo, una cadena con la ruta del fichero, etc.

Puede ser un inconveniente ya que no se realiza control de tipos.

Con el sistema de anotaciones se puede comprobar el tipado de los parámetros.

>>> def f(a:str, b:int)->int:  # Anotación de tipos esperados en parámetros y en retorno
...     print(locals())
...     return 0
... 

>>> f('aaa',32)  # llamada según lo esperado
{'a': 'aaa', 'b': 32}
0

>>> f(32,7)  # llamada contra lo esperado, y se ejecuta igual
{'a': 32, 'b': 7}
0
>>> def f(a:str, b:int)->int:  # Anotación de tipos esperados en parámetros y en retorno
...     print(locals())
...     return 0
... 

>>> f('aaa',32)  # llamada según lo esperado
{'a': 'aaa', 'b': 32}
0

>>> f(32,7)  # llamada contra lo esperado, y se ejecuta igual
{'a': 32, 'b': 7}
0

No es fuertemente tipado, por lo que se ejecutará igual aunque no se cumpla.
La lista de anotaciones para ver los tipos esperados se encuentra en:

>>> f.__annotations__
{'a': <class 'str'>, 'b': <class 'int'>, 'return': <class 'int'>}
>>> f.__annotations__
{'a': <class 'str'>, 'b': <class 'int'>, 'return': <class 'int'>}

Tipos hints

Para dar más información del tipado

PDTE

from typing import Sequence, Mapping

def f(lista: Sequence[str])->Mapping[str,int]:
    pass
from typing import Sequence, Mapping

def f(lista: Sequence[str])->Mapping[str,int]:
    pass

CLASE

Declaración

Firma

class Nombre (clases padre)

>>> class A():
...     pass
... 
>>> type. mro(A)
[<class '__main__.A'>, <class 'object'>]
>>> class A():
...     pass
... 
>>> type. mro(A)
[<class '__main__.A'>, <class 'object'>]

Dice que la clase A es de tipo class y que su clase padre es 'object' (por defecto)

Atributos

Dependen de la indentación.
En python todo es público, los atributos se almacenan en la lista que se accede con la primitiva dir y se accede mediante el punto .

>>> class A():
...     altura = 12

>>> A.altura
12

>>> 'altura' in dir(A)
True
>>> class A():
...     altura = 12

>>> A.altura
12

>>> 'altura' in dir(A)
True

Métodos

En python todo es un objeto, los atributos son objetos, los métodos son objetos, todo.

Se declaran igual que una función, aunque indentados en el bloque del cuerpo de la clase.

>>> class A():
...     altura=12
...     def ff(self): pass
... 
>>> type(A.ff)
<class 'function'>
>>> class A():
...     altura=12
...     def ff(self): pass
... 
>>> type(A.ff)
<class 'function'>

Hay 3 tipos de métodos:

Instanciación

No hay que usar new, simplemente:

>>> a = A()
>>> a.altura
12
>>> a.altura = 10
>>> a.altura
10

>>> b = A()  # otra instancia a la misma clase
>>> b.altura
12

>>> c = a  # dos punteros a la misma instancia
>>> c.altura
10

>>> del a    # elimina el puntero, pero no la instancia
>>> a.altura
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

>>> c.altura  # la instancia sigue activa en el puntero c
10
>>> a = A()
>>> a.altura
12
>>> a.altura = 10
>>> a.altura
10

>>> b = A()  # otra instancia a la misma clase
>>> b.altura
12

>>> c = a  # dos punteros a la misma instancia
>>> c.altura
10

>>> del a    # elimina el puntero, pero no la instancia
>>> a.altura
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

>>> c.altura  # la instancia sigue activa en el puntero c
10

MÓDULO

¿Para qué sirve un módulo?

Un módulo es un conjunto de funcionalidades agrupadas en un mismo bloque.

Se ejecuta directamente y se considera un punto de entrada de la aplicación.

Hay buenas prácticas para su diseño.

Declaración

Puede ser un fichero .py o .pyc (si está compilado).
Un fichero programado en C (para CPython)
O una carpeta.

Es un bloque con su propio espacio de nombres y que contiene código y/o otros módulos.

El archivo init.py es obligatorio en python2 pero no en python3, donde sólo es útil para declarar algunas variables.

Instrucciones específicas

El código de cada módulo es independiente y son estancos: sólo se puede acceder al contenido desde el propio módulo.

Para poder usar un módulo desde otro módulo hay que importarlo, usando import, esa instrucción crea una variable contenedor con el mismo nombre que el módulo, que apunta al módulo (que, como todo en python, también es un objeto). Con esa variable se puede acceder al contenido.

import mi_modulo
mi_modulo.mi_funcion()
import mi_modulo
mi_modulo.mi_funcion()

Importación parcial podemos importar sólo las funciones que nos interesen. En ese caso se crea una variable local con el nombre de la función importada que apunta directamente a esa función

from mi_modulo import mi_funcion
from mi_modulo import mi_funcion

Alias de funciones cambia el nombre de la variable local, usando un alias que seguirá apuntando a la función

>>> from math import sqrt as raiz
>>> raiz
<built-in function sqrt>

>>> raiz(9)
3.0
>>> raiz(2)
1.4142135623730951
>>> from math import sqrt as raiz
>>> raiz
<built-in function sqrt>

>>> raiz(9)
3.0
>>> raiz(2)
1.4142135623730951

Alias de módulo cambia el nombre de la variable local que apunta al módulo importado

>>> import cmath as math_complejos
>>> math_complejos
<module 'cmath' (built-in)>
>>> import cmath as math_complejos
>>> math_complejos
<module 'cmath' (built-in)>

Punto de entrada si el módulo se importa para usar en otro módulo, o si el módulo se ejecuta directamente python3 mi_modulo.py

En el fichero de código tendremos un if para determinar cuando se ejecuta o cuando se importa:

if __name__ == "__main__":
    # instrucciones para cuando el módulo es el punto de entrada
else:
    # instrucciones para cuando el módulo está siendo importado
if __name__ == "__main__":
    # instrucciones para cuando el módulo es el punto de entrada
else:
    # instrucciones para cuando el módulo está siendo importado

Import *
Cuando se importa un módulo usando

from mi_modulo import *
from mi_modulo import *

Se importará todas las funciones/variables/objetos declarados en la variable __all__ al principio de ese módulo:

__all__ = ['foo', 'faa', 'MiClase', ...]
__all__ = ['foo', 'faa', 'MiClase', ...]

Conocer el contenido de un módulo

Lo más seguro es abrir el módulo y leer el código, tambien es lo más lento.

Suponiendo que esté bien documentado, se puede hacer también con dir y help

>>> import cmath
>>> dir(cmath)
['__doc__', '__name__', '__package__', 'acos', 'acosh', 'asin', 'asinh', 'atan',
 'atanh', 'cos', 'cosh', 'e', 'exp', 'isinf', 'isnan', 'log', 'log10', 'phase', 
'pi', 'polar', 'rect', 'sin', 'sinh', 'sqrt', 'tan', 'tanh']
>>> import cmath
>>> dir(cmath)
['__doc__', '__name__', '__package__', 'acos', 'acosh', 'asin', 'asinh', 'atan',
 'atanh', 'cos', 'cosh', 'e', 'exp', 'isinf', 'isnan', 'log', 'log10', 'phase', 
'pi', 'polar', 'rect', 'sin', 'sinh', 'sqrt', 'tan', 'tanh']

Con help(cmath) se abre una ventana man del módulo, y también se puede pedir ayuda específica de una función help(cmath.sqrt)

Compilar un módulo

Al arrancar python se arranca la máquina virtual (M.V.), se carga el módulo de entrada y los módulos de importación (de forma recursiva), para cada uno de ellos se hace un ánalisis sintáctico y se compila a un lenguaje entendible por la M.V.

Se compila todo el módulo aunque sólo se importe una función.

No hay que controlar reimportaciones, aunque se importe dos veces el mismo módulo sólo se compilará la primera vez.

Los compilados tienen extensión .pyc y se procesan en la carpeta __pycache__ para evitar ensuciar las carpetas con archivos no fuentes.

Python comprueba si el código es más reciente que el archivo compilado, sólo volverá a compilar los archivos necesarios.

Un archivo compilador con python 3.3 tiene distinta versión que un archivo compilado con python 3.4.

Dos niveles de optimización

python mi_modulo.py
python -O mi_modulo.py
python -OO mi_modulo.py
python mi_modulo.py
python -O mi_modulo.py
python -OO mi_modulo.py

Dan lugar a nombres de compilados distintos

cpython sólo es una versión de python entre muchas, por eso compila sin machacar otros compilados

Un mismo módulo puede tener muchos compilados: